//
// Created by Ctzzz on 2024/4/28.
//


#include "glad/glad.h"
#include "GLFW/glfw3.h"
#include <stdio.h>
#include <stdbool.h>

/**
 * 当窗口大小改变时，调用此函数
 * @param window
 * @param width
 * @param height
 */
void framebuffer_size_callback(GLFWwindow *window, int width, int height) {
    // 设置视口大小
    glViewport(0, 0, width, height);
}

/**
 * 处理输入
 * @param window
 */
void processInput(GLFWwindow *window) {
    // 如果按下ESC键，设置窗口的windowShouldClose属性为true，关闭窗口
    // glfwGetKey函数接收一个窗口以及一个按键作为输入
    if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS) {
        glfwSetWindowShouldClose(window, true);
    }
}

// 顶点着色器源码
// 使用in关键字，在顶点着色器中声明所有的输入顶点属性(Input Vertex Attribute)。现在我们只关心位置(Position)数据，所以我们只需要一个顶点属性
// 由于每个顶点都有一个3D坐标，我们就创建一个vec3输入变量aPos
// GLSL中一个向量有最多4个分量，每个分量值都代表空间中的一个坐标，它们可以通过vec.x、vec.y、vec.z和vec.w来获取。注意vec.w分量不是用作表达空间中的位置的（我们处理的是3D不是4D），而是用在所谓透视除法(Perspective Division)上
const char *vertexShaderSource = "#version 330 core\n"
                                 "layout (location = 0) in vec3 aPos;\n"
                                 "void main()\n"
                                 "{\n"
                                 "   gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);\n"
                                 "}\0";

// 片段着色器源码
// 片段着色器只需要一个输出变量，这个变量是一个4分量向量，它表示的是最终的输出颜色
// 这个4分量向量表示的是RGBA，所以我们只需要设置前三个分量，最后一个分量设置为1.0
const char *fragmentShaderSource = "#version 330 core\n"
                                   "out vec4 FragColor;\n"
                                   "void main()\n"
                                   "{\n"
                                   "   FragColor = vec4(1.0f, 0.5f, 0.2f, 1.0f);\n"
                                   "}\n\0";

int main() {
    glfwInit();
    // 设置OpenGL版本为3.3
    // 主版本号是3
    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
    // 次版本号是3
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
    // 告诉GLFW我们使用的是核心模式(Core-profile)
    glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
    //glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);

    /*****************************创建窗口**************************************/

    // 创建一个窗口对象,
    // 800x600的窗口,
    // 窗口的名称叫"LearnOpenGL",
    // monitor为NULL,表示不在全屏模式下创建窗口,
    // share为NULL,表示不共享上下文
    GLFWwindow *window = glfwCreateWindow(800, 600, "LearnOpenGL", NULL, NULL);
    if (window == NULL) {
        // 如果窗口创建失败,则输出并释放内存
        printf("Failed to create GLFW window\n");
        // glfwTerminate函数用来释放GLFW分配的资源
        glfwTerminate();
        return -1;
    }
    // 如果创建成功，glfwMakeContextCurrent函数通知GLFW将窗口的上下文设置为当前线程的主上下文
    glfwMakeContextCurrent(window);
    // 设置窗口大小改变时的回调函数
    glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);
    // 初始化GLAD
    if (!gladLoadGLLoader((GLADloadproc) glfwGetProcAddress)) {
        printf("Failed to initialize GLAD\n");
        return -1;
    }

    /*******************************创建顶点着色器*******************************************/
    // 把需要创建的着色器类型以参数形式提供给glCreateShader。由于我们正在创建一个顶点着色器，传递的参数是GL_VERTEX_SHADER
    unsigned int vertexShader = glCreateShader(GL_VERTEX_SHADER);
    // 把着色器源码附加到着色器对象上，然后编译它
    // glShaderSource(), 第一个参数-要编译的着色器对象
    //                   第二个参数-着色器源码字符串的数量，这里我们只有一个
    //                   第三个参数-真正的着色器源码
    //                   第四个参数-先设置为NULL
    glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);
    glCompileShader(vertexShader);
    // 检查编译是否成功
    int success;
    char infoLog[512];
    // glGetShaderiv函数检查是否编译成功，如果编译失败，我们可以调用glGetShaderInfoLog来获取错误信息
    glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &success);
    if (!success) {
        glGetShaderInfoLog(vertexShader, 512, NULL, infoLog);
        printf("ERROR::SHADER::VERTEX::COMPILATION_FAILED\n%s\n", infoLog);
        return -1;
    }

    /*******************************创建片段着色器*******************************************/
    // 创建片段着色器，由于我们正在创建一个片段着色器，传递的参数是GL_FRAGMENT_SHADER
    unsigned int fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
    glShaderSource(fragmentShader, 1, &fragmentShaderSource, NULL);
    glCompileShader(fragmentShader);
    // 检查编译是否成功
    glGetShaderiv(fragmentShader, GL_COMPILE_STATUS, &success);
    if (!success) {
        glGetShaderInfoLog(fragmentShader, 512, NULL, infoLog);
        printf("ERROR::SHADER::FRAGMENT::COMPILATION_FAILED\n%s\n", infoLog);
        return -1;
    }

    /*******************************创建着色器程序*******************************************/
    // 着色器程序对象(Shader Program Object)是多个着色器合并之后并最终链接完成的版本
    // 如果要使用刚创建的着色器，我们必须把它们链接(Link)为一个着色器程序对象
    // 创建一个程序对象
    unsigned int shaderProgram = glCreateProgram();
    // 把之前编译的着色器附加到程序对象上
    glAttachShader(shaderProgram, vertexShader);
    glAttachShader(shaderProgram, fragmentShader);
    // 链接着色器
    glLinkProgram(shaderProgram);
    // 检查链接是否成功
    glGetProgramiv(shaderProgram, GL_LINK_STATUS, &success);
    if (!success) {
        glGetProgramInfoLog(shaderProgram, 512, NULL, infoLog);
        printf("ERROR::SHADER::PROGRAM::LINK_FAILED\n%s\n", infoLog);
        return -1;
    }
//    // 链接成功后，激活着色器程序，以后所有的渲染指令都会使用这个着色器程序
//    glUseProgram(shaderProgram);
    // 激活成功后，删除着色器对象
    glDeleteShader(vertexShader);
    glDeleteShader(fragmentShader);

    /**************到这一步，已经把顶点数据发送给了GPU，并指示了GPU在顶点和片段着色器中处理它************/
    /**************接下来，我们需要设置OpenGL如何解释顶点数据，并且告诉它如何绘制图元************/
    /*******************************设置顶点数据*******************************************/
    /*******************************顶点属性应该被存在顶点数组对象VAO中*******************************************/
    float vertices[] = {
            // 第一个三角形
            -0.5f, -0.5f, 0.0f, // 左
            0.5f, -0.5f, 0.0f, // 右
            0.0f, 0.5f, 0.0f, // 上
    };

    float twoVertices[] = {
            // 第二个三角形
            -0.5f, 0.5f, 0.0f, // 右
            0.0f, -0.5f, 0.0f, // 左
            0.5f, 0.5f, 0.0f, // 上
    };
//    // 顶点属性
//    float vertices[] = {
//            0.5f, 0.5f, 0.0f,   // 右上角
//            0.5f, -0.5f, 0.0f,  // 右下角
//            -0.5f, -0.5f, 0.0f, // 左下角
//            -0.5f, 0.5f, 0.0f   // 左上角
//    };
//    // 索引数据
//    unsigned int indices[] = {
//            // 注意索引从0开始
//            // 此例的索引(0,1,2,3)就是顶点数组vertices的下标，
//            // 这样可以由下标代表顶点组合成矩形
//            0, 1, 3, // 第一个三角形
//            1, 2, 3  // 第二个三角形
//    };

    // 创建一个顶点数组对象VAO，和一个顶点缓冲对象VBO，和一个元素缓冲对象EBO
    unsigned int VAO[2], VBO[2];
    glGenVertexArrays(2, VAO);
    glGenBuffers(2, VBO);
//    glGenBuffers(1, &EBO);
    // 首先绑定VAO
    glBindVertexArray(VAO[0]);
    // 把顶点数组赋值到缓冲中供OpenGL使用
    glBindBuffer(GL_ARRAY_BUFFER, VBO[0]);
    glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

//    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
//    glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
    // 设置顶点属性指针
    // glVertexAttribPointer,告诉OpenGL该如何解析顶点数据
    //          第一个参数-顶点属性位置值，顶点着色器源码中的layout(location = 0)定义了这个值
    //          第二个参数-顶点属性的大小，这里是一个vec3，所以是3
    //          第三个参数-数据的类型，这里是GL_FLOAT(GLSL中vec*是由浮点数值组成的)
    //          第四个参数-是否希望数据被标准化，如果我们设置为GL_TRUE，所有数据都会被映射到0（对于有符号型signed数据是-1）到1之间.
    //          第五个参数-步长(Stride)，它告诉我们在连续的顶点属性组之间的间隔。由于下个位置数据在3个float之后，我们把步长设置为3 * sizeof(float)
    //          第六个参数-位置数据在缓冲中起始位置的偏移量，由于位置数据在数组的开头，所以这里是0
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void *) 0);
    // 启用顶点属性，以顶点属性位置值作为参数，启用顶点属性
    glEnableVertexAttribArray(0);

    // 首先绑定VAO
    glBindVertexArray(VAO[1]);
    // 把顶点数组赋值到缓冲中供OpenGL使用
    glBindBuffer(GL_ARRAY_BUFFER, VBO[1]);
    glBufferData(GL_ARRAY_BUFFER, sizeof(twoVertices), twoVertices, GL_STATIC_DRAW);
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void *) 0);
    // 启用顶点属性，以顶点属性位置值作为参数，启用顶点属性
    glEnableVertexAttribArray(0);

    // 不需要每次都解绑VBO
//    glBindBuffer(GL_ARRAY_BUFFER, 0);
    // glBindVertexArray(0)解绑VAO
//    glBindVertexArray(0);


    // glPolygonMode配置OpenGL如何绘制图元
    //          第一个参数-GL_FRONT_AND_BACK表示我们同时想要填充正面和背面
    //          第二个参数-表示我们希望绘制的图元类型，这里是GL_LINE线框模式
    glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);


    // 渲染循环,
    // 可不希望只绘制一个图像之后我们的应用程序就立即退出并关闭窗口。
    // 我们希望程序在我们主动关闭它之前不断绘制图像并能够接受用户输入。因此，我们需要在程序中添加一个while循环
    // 我们可以把它称之为渲染循环(Render Loop)，它能在我们让GLFW退出前一直保持运行
    // glfwWindowShouldClose函数在我们每次循环的开始前检查一次GLFW是否被要求退出，如果是的话，该函数返回true，渲染循环将停止运行，之后我们就可以关闭应用程序
    while (!glfwWindowShouldClose(window)) {
        // 处理输入
        processInput(window);
        // 渲染指令
        // 还调用了glClearColor来设置清空屏幕所用的颜色
        glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
        // 设置清空屏幕所用的颜色,glClear函数来清空屏幕的缓冲，这里我们只关心颜色缓冲，所以设置为GL_COLOR_BUFFER_BIT
        // GL_COLOR_BUFFER_BIT - 颜色缓冲
        // GL_DEPTH_BUFFER_BIT - 深度缓冲
        // GL_STENCIL_BUFFER_BIT - 模板缓冲
        glClear(GL_COLOR_BUFFER_BIT);

        // 画出第一个三角形
        glUseProgram(shaderProgram);
        glBindVertexArray(VAO[0]);
        // glDrawArrays函数，它使用当前激活的着色器，之前定义的顶点属性配置，和VBO的顶点数据（通过VAO间接绑定）来绘制图元
        //              第一个参数-绘制的图元类型，GL_TRIANGLES表示绘制三角形
        //              第二个参数-顶点数组的起始索引，这里是0
        //              第三个参数-绘制的顶点个数，这里是3个
        glDrawArrays(GL_TRIANGLES, 0, 3);

        glBindVertexArray(VAO[1]);
        // glDrawArrays函数，它使用当前激活的着色器，之前定义的顶点属性配置，和VBO的顶点数据（通过VAO间接绑定）来绘制图元
        //              第一个参数-绘制的图元类型，GL_TRIANGLES表示绘制三角形
        //              第二个参数-顶点数组的起始索引，这里是0
        //              第三个参数-绘制的顶点个数，这里是3个
        glDrawArrays(GL_TRIANGLES, 0, 3);
        // 当使用EBO时
        // glDrawElements函数，它使用当前激活的着色器，之前定义的顶点属性配置，和EBO的索引数据来绘制图元
        //              第一个参数-绘制的图元类型，GL_TRIANGLES表示绘制三角形
        //              第二个参数-绘制的顶点个数，这里是6个
        //              第三个参数-索引的类型，这里是GL_UNSIGNED_INT，因为我们在定义EBO的时候使用的是unsigned int
        //              第四个参数-索引数据的偏移量，这里是0
//        glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
//        glBindVertexArray(0);


        // 交换缓冲区，它在这一迭代中被用来绘制，并且将会作为输出显示在屏幕上
        glfwSwapBuffers(window);
        // 检查有没有触发什么事件(比如键盘输入、鼠标移动等)
        glfwPollEvents();
    }

    glDeleteVertexArrays(2, VAO);
    glDeleteBuffers(2, VBO);
//    glDeleteBuffers(1, &EBO);
    glDeleteProgram(shaderProgram);

    // 循环渲染结束后，释放GLFW分配的内存
    glfwTerminate();

    return 0;
}